"""
One dimension data series
"""
import collections
import itertools
import json
import operator
import os

# python 3 notation
map = itertools.imap
zip = itertools.izip

__all__ = ['Series','ColumnView']


# 3. Data model - Python v2.7.2 documentation
# http://docs.python.org/reference/datamodel.html?highlight=__add__#emulating-numeric-types

vector_operators = [
('__lt__'        , operator.__lt__       , 'L' ),
('__le__'        , operator.__le__       , 'L' ),
('__eq__'        , operator.__eq__       , 'L' ),
('__ne__'        , operator.__ne__       , 'L' ),
('__gt__'        , operator.__gt__       , 'L' ),
('__ge__'        , operator.__ge__       , 'L' ),

('__add__'       , operator.__add__      , 'L' ),
('__sub__'       , operator.__sub__      , 'L' ),
('__mul__'       , operator.__mul__      , 'L' ),
('__floordiv__'  , operator.__floordiv__ , 'L' ),
('__mod__'       , operator.__mod__      , 'L' ),
# __divmod__                                    ,
('__pow__'       , operator.__pow__      , 'L' ), # no modulo argument as operator
('__lshift__'    , operator.__lshift__   , 'L' ),
('__rshift__'    , operator.__rshift__   , 'L' ),
('__and__'       , operator.__and__      , 'L' ),
('__xor__'       , operator.__xor__      , 'L' ),
('__or__'        , operator.__or__       , 'L' ),
('__div__'       , operator.__div__      , 'L' ),
('__truediv__'   , operator.__truediv__  , 'L' ),

('__radd__'      , operator.__add__      , 'R' ),
('__rsub__'      , operator.__sub__      , 'R' ),
('__rmul__'      , operator.__mul__      , 'R' ),
('__rdiv__'      , operator.__div__      , 'R' ),
('__rtruediv__'  , operator.__truediv__  , 'R' ),
('__rfloordiv__' , operator.__floordiv__ , 'R' ),
('__rmod__'      , operator.__mod__      , 'R' ),
# __rdivmod__
('__rpow__'      , operator.__pow__      , 'R' ),
('__rlshift__'   , operator.__lshift__   , 'R' ),
('__rrshift__'   , operator.__rshift__   , 'R' ),
('__rand__'      , operator.__and__      , 'R' ),
('__rxor__'      , operator.__xor__      , 'R' ),
('__ror__'       , operator.__or__       , 'R' ),

('__iadd__'      , operator.__add__      , 'I' ),
('__isub__'      , operator.__sub__      , 'I' ),
('__imul__'      , operator.__mul__      , 'I' ),
('__idiv__'      , operator.__div__      , 'I' ),
('__itruediv__'  , operator.__truediv__  , 'I' ),
('__ifloordiv__' , operator.__floordiv__ , 'I' ),
('__imod__'      , operator.__mod__      , 'I' ),
('__ipow__'      , operator.__pow__      , 'I' ),
('__ilshift__'   , operator.__lshift__   , 'I' ),
('__irshift__'   , operator.__rshift__   , 'I' ),
('__iand__'      , operator.__and__      , 'I' ),
('__ixor__'      , operator.__xor__      , 'I' ),
('__ior__'       , operator.__or__       , 'I' ),

('__neg__'       , operator.__neg__      , 'U' ),
('__pos__'       , operator.__pos__      , 'U' ),
('__abs__'       , operator.__abs__      , 'U' ),

# we handle ~ operator with special method
#('__invert__'    , operator.__invert__   , 'U' ),

]


def create_operator_methods():
    """
    Create a dictionary of object operator methods.
    """
    methods = {}
    for op_name, op, LR in vector_operators:
        if LR == 'L':
            # create new method with op register as default parameter (not closure)
            def op_method(self, other, op=op):
                other = self._prepare_other_operand(other)
                return Series(map(op, self, other))

        elif LR == 'R':
            # the right operand version
            def op_method(self, other, op=op):
                other = self._prepare_other_operand(other)
                return Series(map(op, other, self))

        elif LR == 'I':
            # the augmented arithmetic assignments
            def op_method(self, other, op=op):
                other = self._prepare_other_operand(other)
                self[:] = map(op, self, other)
                return self

        elif LR == 'U':
            # the unary operator
            def op_method(self, op=op):
                return Series(map(op, self))

        else:
            raise ValueError(LR)

        methods[op_name] = op_method

    return methods



class BaseSeries(collections.Sequence):
    """
    Basic sequence that supports VectorOperations.
    """

    def _prepare_other_operand(self, other):
        if isinstance(other, collections.Sequence) and (not isinstance(other, basestring)):
            # is there any thign to do if the length is different?
            if len(self) != len(other):
                raise ValueError('Requires operands of the same size: %s, %s' % (len(self), len(other)))
            return other
        else:
            return itertools.repeat(other, len(self))

    locals().update(create_operator_methods())


    def __invert__(self):
        if all(isinstance(val, bool) for val in self):
            # for boolean series, we commandeer the bitwise ~ operator as boolean not operator.
            return Series(not val for val in self)
        else:
            return Series(~val for val in self)


    def __bool__(self):
        # Block truth value testing because it is ambiguous. There is also a
        # very error prone problem when using boolean operator like and, or, not.
        # Since we cannot override builtin Python operator and its short circuit
        # behavior, the result is misleading. Blocking it altogether minimize
        # the risk. There is little downside that I'm aware of.
        raise TypeError("Truth value testing on Series is ambiguous. Apply explicit test like len, any or all. For Boolean operations, use &, |, ~ instead of and, or, not.")

    # Python 2.x compatibility
    __nonzero__ = __bool__


    def __getattr__(self, name):
        return Series(getattr(item ,name) for item in self)


    def __call__(self, *args, **kwargs):
        return Series(item(*args, **kwargs) for item in self)


    def map(self, func):
        """ map <func> to the series.

        series.map(func) is similar to map(series, func) except
        it return a Series instead of list.
        """
        return Series(map(func, self))


    def json_dumps(self):
        """ Encode as JSON string. Assume elements are JSON primitive objects """
        return json.dumps(list(self))


    def __repr__(self):
        if len(self) > 100:
            return 'Series[%s, ... ,%s]' % (','.join(map(str,self[:100])), self[-1])
        else:
            return 'Series[%s]' % ','.join(map(str,self))



class Series(BaseSeries):

    def __init__(self, iterable=[]):
        self.data = list(iterable)

    def __len__(self):
        return len(self.data)

    def __iter__(self):
        return iter(self.data)

    # TODO: returns a list when we slice it. Should we return a Series? Or a sub-Series as a view?
    def __getitem__(self, key):
        return self.data[key]

    # TODO: slice assignment has same semantics as list for now, i.e. you can
    # chance a list's size by assign a slice of different size. Right now Series
    # is used as intermediate result of vector operations. It seems we can get
    # away with not enforcing the ColumnView's not size change semantics. Also
    # no support of assigning scalar.
    def __setitem__(self, key, value):
        self.data[key] = value



class ColumnView(BaseSeries):
    """
    The column view on a relation.
    """
    def __init__(self, relation, idx):
        self.relation = relation
        self.idx = idx

    def __len__(self):
        return len(self.relation)

    def __iter__(self):
        idx = self.idx
        return (t[idx] for t in self.relation)

    def __getitem__(self, key):
        idx = self.idx
        if isinstance(key,int):
            return self.relation[key][idx]
        else:
            return [t[idx] for t in self.relation[key]]

    def __setitem__(self, key, value):
        idx = self.idx
        if isinstance(key,slice):
            self._slice_assignment(key, value)
        else:
            self.relation[key][idx] = value

    def _slice_assignment(self, key, value):
        """
        Do - self.relation[key] = value

        Validate the array size won't change after assignment.
        Also support assignment of scalar value.
        """
        indices = key.indices(len(self.relation))
        # find out the slice size  `k`
        k, mod = divmod(indices[1] - indices[0], indices[2])
        if mod:
            k += 1
        # negative length means 0 length
        k = max(k,0)

        if isinstance(value, collections.Iterator):
            # turn iterator into list to find the length for validation
            value = list(value)
            # Python does this in slice assignment also.
            # Notice the time lack in the experiment below
            #
            # >>> l=range(10); l[1::2] = xrange(10000000)

        if isinstance(value, basestring) or (not isinstance(value, collections.Sequence)):
            # broadcast scalar to the whole slice
            value = itertools.repeat(value)

        else:
            # value is a sequence, check size
            if k != len(value):
                raise ValueError('attemp to assign sequence of size %s to extended slice of size %s' % (len(value), k))

        idx = self.idx
        for row, v in zip(self.relation[key], value):
            row[idx] = v

